﻿using System;
using System.Collections.Generic;
using System.Text;
using Engine.Dtd.Schema;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using System.Diagnostics;
using Engine.Sgml.Validation;
using Engine.Sgml.Validation.Rules;
using Engine.Sgml.Configuration;
using Engine.Sgml.XPath;
using System.Text.RegularExpressions;
using System.Reflection;
using Engine.Sgml.Decoding;
using Engine.Sgml.Parser;


namespace Engine.Sgml {
    /// <summary>
    /// Represents SGML document.
    /// </summary>
    public class SgmlDocument : IXPathNavigable {
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private static object lockObject = new object();
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private static DtdSchema embeddedSchema;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private UrlResolver urlResolver;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private DocumentValidator validator;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private SElement rootElement;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private SgmlNavigator navigator;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private SgmlParser parser;
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private TraceSource traceSource;

        /// <summary>
        /// Gets the root element of the tree for this document.
        /// </summary>
        ///  /// <remarks>
        /// A document object that, as the root of the document tree, provides access
        /// to the entire SGML document.
        /// </remarks>
        public SElement Root {
            get { return rootElement; }
            }

        /// <summary>
        /// Gets a set of features to support SGML validation engine.
        /// </summary>
        public ValidatorBehavior ValidatorBehavior { get; private set; }

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Encoding defaultEncoding;
        /// <summary>
        /// Gets or sets the default encoding to ensure that encoded bytes
        /// are decoded properly.
        /// </summary>
        /// <remarks>
        /// The SGML Document has the "Auto Detect" feature for character encoding.
        /// When it is impossible to detect the character encoding or it doesn't work properly
        /// the specified encoding will be used.
        /// </remarks>
        /// <exception cref="System.ArgumentNullException">If a value is null.</exception>
        public Encoding DefaultEncoding {
            get { return defaultEncoding; }
            set {
                Guard.ArgumentNotNull(value, "value");
                defaultEncoding = value;
                }
            }

        /// <summary>
        /// Gets the <see cref="System.Diagnostics.TraceSource"/> object that enable this library to trace execution of code 
        /// and associate trace messages with their source. 
        /// </summary>
        /// <remarks>
        /// <para>
        /// Trace allows you to monitor the grammar and structure of your SGML document in real-life settings.
        /// Enabled tracing offers detailed information about any SGML validation errors in your document, helps you isolate problems and fix them.
        /// </para>
        /// <para>
        /// Trace output is emitted using the <see cref="System.Diagnostics.TraceSource"/> class and can be controlled by configuration file settings.
        /// </para>
        /// </remarks>
        /// <seealso cref="System.Diagnostics.TraceSource"/>
        public TraceSource TraceSource {
            get { return traceSource; }
            }

        /// <summary>
        /// Creates the XML reader for root element of the tree of this document.
        /// </summary>
        /// <returns>An XmlReader that can be used to read the root and its descendants of this document.</returns>
        public XmlReader CreateReader() {
            return new XmlTextReader(rootElement);
            }

        /// <summary>
        /// Creates an XPath navigator for navigating this document.
        /// </summary>
        /// <returns>An XPathNavigator object.</returns>
        public XPathNavigator CreateNavigator() {
            return navigator;
            }

        /// <summary>
        /// Selects an element that matches the XPath expression.
        /// </summary>
        /// <param name="expression">A string that contains the XPath expression.</param>
        /// <returns>A SElement or null.</returns>
        /// <exception cref="System.ArgumentNullException">If an <paramref name="expression"/> is null.</exception>
        public SElement SelectElement(string expression) {
            Guard.ArgumentNotNull(expression, "expression");
            return navigator.SelectElement(expression);
            }

        /// <summary>
        /// Selects an attribute using a XPath expression.
        /// </summary>
        /// <param name="expression">A string that contains a XPath expression.</param>
        /// <returns>A SAttribute or null.</returns>
        /// <exception cref="System.ArgumentNullException">If an <paramref name="expression"/> is null.</exception>
        public SAttribute SelectAttribute(string expression) {
            Guard.ArgumentNotNull(expression, "expression");
            return navigator.SelectAttribute(expression);
            }

        /// <summary>
        /// Selects a collection of elements matching the XPath expression.
        /// </summary>
        /// <param name="expression">A string that contains the XPath expression.</param>
        /// <returns>An IEnumerable&lt;SElement&gt; that contains the selected elements.</returns>
        /// <exception cref="System.ArgumentNullException">If an <paramref name="expression"/> is null.</exception>
        public IEnumerable<SElement> SelectElements(string expression) {
            Guard.ArgumentNotNull(expression, "expression");
            return navigator.SelectElements(expression);
            }

        /// <summary>
        /// Selects a collection of attributes using a XPath expression.
        /// </summary>
        /// <param name="expression">A string that contains a XPath expression.</param>
        /// <returns>An IEnumerable&lt;SAttribute&gt; that contains the selected elements.</returns>
        /// <exception cref="System.ArgumentNullException">If an <paramref name="expression"/> is null.</exception>
        public IEnumerable<SAttribute> SelectAttributes(string expression) {
            Guard.ArgumentNotNull(expression, "expression");
            return navigator.SelectAttributes(expression);
            }

        /// <summary>
        /// Initialize a new instance of the SgmlDocument class with specified DTD schema.
        /// </summary>
        /// <param name="schema">A DTD scheme object for this document.</param>
        /// <remarks>
        /// <para>
        /// The DTD validation is enabled by default. If your application don't requires DTD processing, you can disable this by <see cref="ValidatorBehavior"/> class.
        /// </para>
        /// </remarks>
        /// <exception cref="System.ArgumentNullException">If an <paramref name="schema"/> is null.</exception>
        public SgmlDocument(DtdSchema schema) {
            Guard.ArgumentNotNull(schema, "schema");
            this.defaultEncoding = Encoding.UTF8;
            this.traceSource = new TraceSource("Eskulap");
            this.urlResolver = new UrlResolver();
            this.rootElement = new SElement(".", SgmlNodeType.Document);
            this.navigator = new SgmlNavigator(rootElement);
            this.ValidatorBehavior = new ValidatorBehavior();
            this.validator = new DocumentValidator(this, schema);
            UpdateSchemaContext(schema);
            this.parser = new SgmlParser(this, validator.ValidationRules);
            Debug.Assert(ValidatorBehavior.Rules != null);
            }

        /// <summary>
        /// Creates the document and loads the default DTD schema from
        /// manifest resource of this assembly.
        /// </summary>
        public SgmlDocument()
            : this(LoadSchemaFromManifest()) {
            }

        /// <summary>
        /// Loads a content for this document from the specified
        /// resource identifier.
        /// </summary>
        /// <param name="url">The uniform resource identifier that references the file or web resource to load into this document.</param>
        public void Load(Uri url) {
            Load(url, HttpRequestSettings.DefaultSettings, schema);
            }

        /// <summary>
        /// Loads a content for this document from the specified resource identifier and
        /// set the settings for HTTP request.
        /// </summary>
        /// <param name="url">The uniform resource identifier that references the file or web resource to load into this document.</param>
        /// <param name="settings">A WebRequestSettings object.</param>
        public void Load(Uri url, HttpRequestSettings settings) {
            Load(url, settings, schema);
            }

        /// <summary>
        /// Loads a content for this document from the specified resource identifier.
        /// </summary>
        /// <param name="url">The uniform resource identifier that references the file or web resource to load into this document.</param>
        /// <param name="schema">A DTD scheme for this document.</param>
        public void Load(Uri url, DtdSchema schema) {
            Load(url, HttpRequestSettings.DefaultSettings, schema);
            }

        /// <summary>
        /// Loads a content for this document from the specified resource identifier.
        /// </summary>
        /// <param name="url">The uniform resource identifier that references the file or web resource to load into this document.</param>
        /// <param name="settings">A settings for HTTP request.</param>
        /// <param name="schema">A DTD scheme for this document.</param>
        /// <exception cref="System.ArgumentNullException">If an <paramref name="url"/>, <paramref name="settings"/> or <paramref name="schema"/> are null.</exception>
        public void Load(Uri url, HttpRequestSettings settings, DtdSchema schema) {
            Guard.ArgumentNotNull(url, "url");
            Guard.ArgumentNotNull(settings, "settings");
            Guard.ArgumentNotNull(schema, "schema");
            if (!url.IsAbsoluteUri)
                throw new ArgumentException("The url (uniform resource locator) must be absolute.");
            if (!object.ReferenceEquals(this.schema, schema))
                UpdateSchemaContext(schema);

            UpdateDocumentType();

            this.url = url;
            this.urlResolver.DefaultEncoding = this.DefaultEncoding;
            string contentType;

            using (SgmlStream stream = urlResolver.GetEntity(Url, settings, out contentType)) {
                Encoding = stream.Encoding;
                this.url = urlResolver.ResolvedUrl;
                if (contentType != null && SgmlDocumentType.Unknown == Detector.DetectDocumentType(contentType)) {
                    rootElement.Nodes.Clear();
                    SElement text = rootElement.Nodes.Push();
                    text.Clear();
                    text.Name = DName.PCData.ToString();
                    text.NodeType = SgmlNodeType.Text;
                    text.DName = DName.PCData;
                    text.Value = stream.ReadToEnd();
                    }
                else
                    LoadInternal(stream);
                }
            }

        /// <summary>
        /// Loads a content for this document from a System.IO.TextReader.
        /// </summary>
        /// <param name="stream">A TextReader object that will be read content for this document.</param>
        public void Load(TextReader stream) {
            Load(stream, schema);
            }

        /// <summary>
        /// Loads a content for this document from a System.IO.TextReader.
        /// </summary>
        /// <param name="stream">A TextReader object that will be read content for this document.</param>
        /// <param name="schema">A DTD schema for this document.</param>
        public void Load(TextReader stream, DtdSchema schema) {
            Guard.ArgumentNotNull(stream, "reader");
            Guard.ArgumentNotNull(schema, "schema");
            this.url = new Uri(Directory.GetCurrentDirectory());
            if (!object.ReferenceEquals(this.schema, schema))
                UpdateSchemaContext(schema);
            UpdateDocumentType();
            rootElement.Nodes.Clear();
            parser.Parse(stream);
            if (CanValidate())
                validator.Validate(this);
            }


        /// <summary>
        /// Loads a content for this document from a System.IO.Stream object.
        /// </summary>
        /// <param name="stream">A Stream object that contains the SGML data.</param>
        /// <exception cref="System.ArgumentNullException">If a <paramref name="stream"/> is null.</exception>
        public void Load(Stream stream) {
            Guard.ArgumentNotNull(stream, "stream");
            Load(stream, schema);
            }

        /// <summary>
        /// Loads a content for this document from a System.IO.Stream.
        /// </summary>
        /// <param name="stream">The stream that contains the SGML data.</param>
        /// <param name="schema">A DTD scheme for this document.</param>
        /// <exception cref="System.ArgumentNullException">If a <paramref name="stream"/> or <paramref name="schema"/> are null.</exception>
        public void Load(Stream stream, DtdSchema schema) {
            Guard.ArgumentNotNull(stream, "stream");
            Guard.ArgumentNotNull(schema, "schema");
            this.url = new Uri(Directory.GetCurrentDirectory());
            if (!object.ReferenceEquals(this.schema, schema))
                UpdateSchemaContext(schema);
            UpdateDocumentType();
            LoadInternal(new SgmlStream(stream, DefaultEncoding));
            }

        private void LoadInternal(SgmlStream stream) {
            rootElement.Nodes.Clear();
            Encoding = stream.DetectEncoding(parser.Context, rootElement);
            rootElement.Nodes.Clear();
            parser.Parse(stream);
            if (CanValidate()) {
                validator.Validate(this);
                // There are a lot of short live objects which shold be
                // immediatly collected by garbage collector
                // ~3000 bytes
                // GC.Collect();
                }
            }

        private void UpdateSchemaContext(DtdSchema schema) {
            Debug.Assert(schema != null);
            this.schema = schema;
            if (!schema.RootDeclaration.ContentModel.Inclusions.Contains(InclusionNode.Instance.Name))
                schema.RootDeclaration.ContentModel.Inclusions.Add(InclusionNode.Instance.Name);
            if (!ValidatorBehavior.DtdDisabled)
                validator.ValidationRules.Initialize(schema, ValidatorBehavior);
            }

        private void UpdateDocumentType() {
            if (Schema.Name == Schema.Names["html", false])
                DocumentType = SgmlDocumentType.Html;
            else
                DocumentType = SgmlDocumentType.Unknown;
            }

        /// <summary>   
        /// Loads the DTD schema from the manifest resource of this assembly.
        /// </summary>
        /// <returns>A <see cref="Engine.Dtd.Schema.DtdSchema"/> instance that encapsulates the DTD.</returns>
        internal static DtdSchema LoadSchemaFromManifest() {
            if (embeddedSchema == null)
                lock (lockObject) {
                    if (embeddedSchema == null) {
                        Assembly assembly = Assembly.GetExecutingAssembly();
                        using (Stream stream = assembly.GetManifestResourceStream(string.Format("{0}.Schema.Loose.dtd", assembly.GetName().Name))) {
                            if (stream == null)
                                throw new InvalidOperationException("Could not found internal DTD (document type definition) resource.");
                            embeddedSchema = DtdSchema.Load(new StreamReader(stream), new Uri(assembly.CodeBase), "HTML");
                            }
                        }
                    }
            return embeddedSchema;
            }

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Uri url;
        /// <summary>
        /// Gets the base unique resource identifier for this document.
        /// </summary>
        public Uri Url {
            get { return url; }
            }

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private DtdSchema schema;
        /// <summary>
        /// Gets the schema object associated with this document.
        /// </summary>
        public DtdSchema Schema {
            get { return schema; }
            }

        /// <summary>
        /// Gets a value indicating whether the schema was loaded from the manifest of this assembly.
        /// </summary>
        internal bool IsSchemaInternal {
            get { return embeddedSchema == schema; }
            }

        internal SgmlDocumentType DocumentType { get; set; }

        internal bool CanValidate() {
            switch (DocumentType) {
                case SgmlDocumentType.Unknown:
                case SgmlDocumentType.Html:
                    return !ValidatorBehavior.DtdDisabled;
                case SgmlDocumentType.Xml:
                    return !IsSchemaInternal && !ValidatorBehavior.DtdDisabled;
                default:
                    throw new InvalidOperationException("Unknown the type of a the document.");
                }
            }

        /// <summary>
        /// Gets the auto detected encoding in which this document is written or
        /// when the auto detect feature was unable to detect the character encoding,
        /// returns the default encoding.
        /// This property can gets the null reference when the auto detect feature
        /// was never invoked.
        /// </summary>
        public Encoding Encoding { get; private set; }

        /// <summary>
        /// Outputs this document to the specified text writer.
        /// </summary>
        /// <param name="writer">A text writer that this document will be written.</param>
        /// <exception cref="System.ArgumentNullException">If a <paramref name="writer"/> is null.</exception>
        public void Save(TextWriter writer) {
            Guard.ArgumentNotNull(writer, "writer");
            SgmlTextWriter w = null;
            if (ValidatorBehavior.PreserveWhitespace)
                w = new SgmlTextWriter(writer);
            else
                w = new SgmlTextFormatWriter(writer);
            w.WriteDocument(rootElement);
            w.Flush();
            }

        /// <summary>
        /// Outputs this document to the specified stream.
        /// </summary>
        /// <param name="stream">The stream to output this document to.</param>
        /// <param name="encoding">The character encoding.</param>
        /// <exception cref="System.ArgumentNullException">If a <paramref name="stream"/> or <paramref name="encoding"/> is null.</exception> 
        public void Save(Stream stream, Encoding encoding) {
            Guard.ArgumentNotNull(stream, "stream");
            Guard.ArgumentNotNull(encoding, "encoding");
            Save(new StreamWriter(stream, encoding));
            }

        /// <summary>
        /// Outputs this document to the specified stream.
        /// </summary>
        /// <param name="stream">The stream to output this document to.</param>
        /// <exception cref="System.ArgumentNullException">If a <paramref name="stream"/> is null.</exception>
        public void Save(Stream stream) {
            Guard.ArgumentNotNull(stream, "stream");
            if (Encoding == null)
                Save(stream, DefaultEncoding);
            else
                Save(stream, Encoding);
            }

        /// <summary>
        /// Provides the document as formatted SGML string.
        /// </summary>
        /// <returns>A string containing the indented SGML.</returns>
        public override string ToString() {
            using (SgmlTextFormatWriter writer = new SgmlTextFormatWriter()) {
                writer.WriteDocument(rootElement);
                writer.Flush();
                return writer.ToString();
                }
            }

        /// <summary>
        /// Provides the document as SGML string, optionally disabling formatting.
        /// </summary>
        /// <param name="disableFormatting">A boolean value that indicate disable formatting.</param>
        /// <remarks>
        /// To save all significant and insignificant whitespaces in the document output 
        /// set the <see cref="Engine.Sgml.Configuration.ValidatorBehavior.PreserveWhitespace"/> to true.
        /// </remarks>
        /// <returns>A string containing the indented SGML.</returns>
        public string ToString(bool disableFormatting) {
            SgmlTextWriter writer = null;
            try {
                if (disableFormatting)
                    writer = new SgmlTextWriter();
                else
                    writer = new SgmlTextFormatWriter();
                writer.WriteDocument(rootElement);
                writer.Flush();
                return writer.ToString();
                }
            finally {
                writer.Dispose();
                }
            }

        /// <summary>
        /// Provides the document as a formatted XML string.
        /// </summary>
        /// <returns>A System.String containing the indented XML.</returns>
        internal string ToXml() {
            using (XmlReader reader = CreateReader()) {
                StringBuilder builder = new StringBuilder();
                if (reader.Read()) {
                    while (reader.ReadState == ReadState.Interactive)
                        builder.Append(reader.ReadOuterXml());
                    }
                return builder.ToString();
                }
            }
        }
    }
